// (c) Copyright slimCODE Software Inc. - www.slimcode.com
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System;
using System.Security.Cryptography;

namespace SlimCode.Utils
{
  internal class Rfc2898DeriveBytes
  {
    #region PRIVATE STATIC METHODS

    internal static byte[] IntToBigEndianArray( int value )
    {
      byte[] converted = BitConverter.GetBytes( value );

      if( BitConverter.IsLittleEndian )
      {
        byte swap = converted[ 3 ];
        converted[ 3 ] = converted[ 0 ];
        converted[ 0 ] = swap;
        swap = converted[ 2 ];
        converted[ 2 ] = converted[ 1 ];
        converted[ 1 ] = swap;
      }

      return converted;
    }

    #endregion

    #region CONSTRUCTORS

    public Rfc2898DeriveBytes( string password, byte[] salt )
      : this( password, salt, 1000 )
    {
    }

    public Rfc2898DeriveBytes( string password, byte[] salt, int iterations )
    {
      if( password == null )
        throw new ArgumentNullException( "password" );

      if( password.Length == 0 )
        throw new ArgumentException( "The password cannot be empty." );

      if( salt == null )
        throw new ArgumentNullException( "salt" );

      if( salt.Length == 0 )
        throw new ArgumentException( "The salt cannot be empty." );

      if( iterations < 10 )
        throw new ArgumentOutOfRangeException( "Iterations must be greater than 9." );

      m_hmacSha1 = new HMACSHA1( System.Text.Encoding.UTF8.GetBytes( password ) );

      m_salt = salt.Clone() as byte[];
      m_iterations = iterations;

      m_startIndex = m_endIndex = 0;
    }

    #endregion

    #region PUBLIC METHODS

    public byte[] GetBytes( int count )
    {
      if( count <= 0 )
        throw new ArgumentOutOfRangeException( "count" );

      byte[] result = new byte[ count ];

      int copied = 0;

      while( copied < count )
      {
        int toCopy = count - copied;

        while( toCopy > ( m_endIndex - m_startIndex ) )
        {
          toCopy = m_endIndex - m_startIndex;

          if( toCopy == 0 )
          {
            this.UpdateBuffer();
            toCopy = count - copied;
          }
        }

        Buffer.BlockCopy( m_buffer, m_startIndex, result, copied, toCopy );

        m_startIndex += toCopy;
        copied += toCopy;
      }

      return result;
    }

    #endregion

    #region PRIVATE METHODS

    private void UpdateBuffer()
    {
      byte[] blockCount = IntToBigEndianArray( m_block++ );

      m_hmacSha1.TransformBlock( m_salt, 0, m_salt.Length, m_salt, 0 );
      m_hmacSha1.TransformFinalBlock( blockCount, 0, blockCount.Length );

      byte[] hash = m_hmacSha1.Hash;

      m_hmacSha1.Initialize();

      m_buffer = hash.Clone() as byte[];
      m_startIndex = 0;
      m_endIndex = BlockSize;

      // I don't like this hardcoded 20.
      System.Diagnostics.Debug.Assert( m_buffer.Length == BlockSize );

      for( int i = 2; i <= m_iterations; i++ )
      {
        hash = m_hmacSha1.ComputeHash( hash );

        for( int j = 0; j < BlockSize; j++ )
        {
          m_buffer[ j ] = unchecked( ( byte )( m_buffer[ j ] ^ hash[ j ] ) );
        }
      }
    }

    #endregion

    #region PRIVATE FIELDS

    // 160 bits hash size in HMACSHA1.
    private const int BlockSize = 20;

    private HMACSHA1 m_hmacSha1; // = null
    private byte[] m_salt; // = null
    private int m_iterations; // = 0

    private byte[] m_buffer; // = null
    private int m_block = 1;
    private int m_endIndex; // = 0
    private int m_startIndex; // = 0

    #endregion
  }
}
